using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Collections;
using DataBroker.Dal;

namespace DataBroker.Criteria
{
    /// <summary>
    /// The Filter class is used to define which records to return from the 
    /// database. In its simplest form, it will simply return all of them. To create
    /// criteria, <see cref="Clause">Search Bindings</see> are used. Search Bindings
    /// relate a field to a value, and can also be used to create correlated queries and
    /// execute D2Bk column functions (such as MAX, AVG).  By default, each
    /// new Search Binding is ANDed with the previous value. In order to define ORs, the recommended
    /// procedure is to create another Filter object and then add it to the first one
    /// using a <see cref="Logical">Search Boolean</see> of type Or.
    /// The following example will return customers with CustomerID equals to "ALFKI":
    /// <code>
    /// 			Filter sc = new Filter();
    ///             sc.Add("CustomerID","ALFKI");
    ///             List&lt;Entity&gt; customers = D2Bk.Fetch(sc,typeof(Customers));
    /// </code>
    /// The following example will return customer with CustomerID equals to "ALFKI" or with
    /// CustomerID equals to "ANATR":
    /// <code>
    /// 			Filter sc = new Filter();
    ///				Filter sc2 = new Filter();
    ///				sc.Add("CustomerID","ALFKI");
    ///				sc2.Add("CustomerID","ANATR");
    ///				sc.AddNested(sc2, Logical.Or);
    ///				List&lt;Entity&gt; customers = D2Bk.Fetch(sc,typeof(Customers));
    /// </code>
    /// The same results as above can be obtained by using the In operator:
    /// <code>
    /// 			Filter sc = new Filter();
    /// 			sc.Add("customerID","ALFKI,ANATR", Operator.In);
    /// 			List&lt;Entity&gt; customers = D2Bk.Fetch(sc,typeof(Customers));
    /// </code>
    /// </summary>
    /// <remarks>See the NUnit test cases for more examples.</remarks>
    [Serializable]
    public class Filter : ICloneable
    {

        private static readonly log4net.ILog log =
            log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
        // --------------------------------------------------------- INSTANCE VARIABLES

        /// <summary>
        /// Bindings of searchable attributes and their values.
        /// </summary>
        private List<Clause> _bindings = new List<Clause>();

        /// <summary>
        /// Any attributes that should be projectioned with the query if the
        /// data store does not automatically pull out all attributes.
        /// </summary>
        private List<string> _projections = new List<string>();

        /// <summary>
        /// Fields by which the results should be sorted.
        /// </summary>
        private List<string> _sorts = new List<string>();

        /// <summary>
        /// Fields by which the results should be sorted.
        /// </summary>
        private string _custom;

        /// <summary>
        /// Flag for DISTINCT.
        /// </summary>
        private bool _isDistinct;

        /// <summary>
        /// Number for TOP.
        /// </summary>
        private int _top = -1;

        /// <summary>
        /// Maximum number of rows
        /// </summary>
        private long _maxRows = 1000;

        /// <summary>
        /// Number of rows per retrieval
        /// </summary>
        /// <remarks>Default = -1 (no limit)</remarks>
        private int _rowsPerPage = -1;

        /// <summary>
        /// Number of rows per retrieval
        /// </summary>
        public int RowsPerPage
        {
            get
            {
                return _rowsPerPage;
            }
            set
            {
                _rowsPerPage = value;
            }
        }

        ///<summary>
        ///</summary>
        public ReadOnlyCollection<object> CurrentBindings
        {
            get
            {
                return new ReadOnlyCollection<object>(_bindings.ToArray());
            }
        }

        private int _pageNumber = -1;
        /// <summary>
        /// Current page number. When used with the <see cref="RowsPerPage">RowsPerPage</see> 
        /// property, returns records corresponding to the page number selected.
        /// </summary>
        /// <remarks>Default = -1 (no page number)</remarks>
        public int PageNumber
        {
            get
            {
                return _pageNumber;
            }
            set
            {
                _pageNumber = value;
            }
        }

        // --------------------------------------------------------- CLASS CONSTRUCTORS    

        /// <summary>
        /// Default constructor. Assume all fields for entity will be used
        /// in query. The MaxRows setting can be used to limit the amount of
        /// rows returned for the query.
        /// </summary>
        public Filter()
        {
            try
            {
                D2BkSettingsCollection settings = new D2BkSettingsCollection();
                string sMaxRows = settings["MaxRows"];
                if (!string.IsNullOrEmpty(sMaxRows))
                    _maxRows = long.Parse(sMaxRows, CultureInfo.InvariantCulture);
            }
            catch (ArgumentNullException e)
            {
                log.Error(e.Message);
            }
            catch (FormatException e)
            {
                log.Error(e.Message);
            }
            catch (OverflowException e)
            {
                log.Error(e.Message);
            }
        }

        /// <summary>
        /// Constructor that takes a boolean Distinct to add a DISTINCT clause
        /// to query.
        /// </summary>
        public Filter(bool isDistinct)
        {
            _isDistinct = isDistinct;
        }

        /// <summary>
        /// Constructor that accepts a list of fields. This can be used to 
        /// optimize the amount of data returned from the database, although
        /// it is not recommended because the objects returned cannot be
        /// later updated back in the database.
        /// The typical usage of the projections is to define a function.
        /// </summary>
        /// <param name="projections">Fields to be used in query.</param>
        public Filter(string[] projections)
        {
            for (int i = 0; i < projections.Length; i++)
            {
                _projections.Add(projections[i]);
            }
        }

        /// <summary>
        /// Constructor that accepts a Clause object.
        /// </summary>
        /// <param name="clause">SearchBinding object.</param>
        public Filter(Clause clause)
        {
            Add(clause);
        }

        /// <summary>
        /// Adds a comparison to the list of bindings.
        /// </summary>
        /// <param name="clause">Comparison binding to add.</param>
        public void Add(Clause clause)
        {
            _bindings.Add(clause);
        } // END OF Add



        /// <summary>
        /// Adds a list of fields to the query.
        /// </summary>
        /// <param name="projections">ArrayList of fields.</param>
        public void AddProjection(List<string> projections)
        {
            _projections = projections;
        } // END OF AddProjection


        /// <summary>
        /// Adds a list of fields to the query.
        /// </summary>
        /// <param name="projections">String vector of fields.</param>
        public void AddProjection(params string[] projections)
        {
            for (int i = 0; i < projections.Length; i++)
            {
                _projections.Add(projections[i]);
            }
        } // END OF AddProjection
                
        /////<summary>
        ///// Add field to query
        /////</summary>
        /////<param name="projection"></param>
        //public void AddProjection(params string[] projection)
        //{
        //    _projections.Add(projection);
        //}

        /// <summary>
        /// Adds another Filter object to the list.
        /// </summary>
        /// <param name="filter">Search criteria to add.</param>
        public void AddNested(Filter filter)
        {
            //don't allow nesting with itself
            if (filter == this)
                return;
            if (filter._bindings.Count > 0 || filter._projections.Count > 0 || filter._sorts.Count > 0)
            {
                _bindings.Add(new Clause(filter));
            }
        } // END OF Add

        /// <summary>
        /// Add another Filter to create a nested criteria. Use this method when
        /// creating clauses that must be in parenthesis (normally for Or).
        /// </summary>
        /// <param name="filter">Inner search criteria</param>
        /// <param name="field"></param>
        /// <param name="operator"></param>
        /// <param name="entityType"></param>
        public void AddNested(Filter filter, string field, Operator @operator, Type entityType)
        {
            _bindings.Add(new Clause(field, @operator, filter, entityType));
        }

        /// <summary>
        /// Adds a comparison to the list with boolean search operator.
        /// </summary>
        /// <param name="logical">Boolean connector for search.</param>
        /// <param name="filter">Search criteria to add.</param>
        public void AddNested(Filter filter, Logical logical)
        {
            _bindings.Add(new Clause(filter, logical));
        } // END OF Add



        ///<summary>
        ///</summary>
        ///<param name="field"></param>
        ///<param name="fieldValue"></param>
        public void Add(Enum field, object fieldValue)
        {
            Add(field.ToString(), fieldValue);
        }

        /// <summary>
        /// Adds a comparison to the list.
        /// </summary>
        /// <param name="field">Field to compare.</param>
        /// <param name="fieldValue">Value to compare.</param>
        public void Add(string field, object fieldValue)
        {
            if (fieldValue == null)
                return;
            if (fieldValue is string && ((string)fieldValue).Trim().Length == 0)
                return;

            if (fieldValue is Operator)
            {
                if (fieldValue.ToString() == "NULL")
                {
                    /***
                     * Implement the use of NULL in stored procedure parameters
                     ******/
                    _bindings.Add(new Clause(field, ""));
                }
                else
                {
                    Add(field, Logical.And, (Operator)fieldValue);
                }
            }
            else
            {
                if (fieldValue is DateTime)
                {
                    if ((DateTime)fieldValue == DateTime.MinValue)
                    {
                        _bindings.Add(new Clause(Logical.And, field, Operator.IsNull, fieldValue));
                    }
                    else
                    {
                        _bindings.Add(new Clause(field, fieldValue));
                    }
                }
                else
                {
                    _bindings.Add(new Clause(field, fieldValue));
                }
            }
        }// END OF Add


        /// <summary>
        /// Adds a comparison to the list. Allows the use of Empty strings
        /// in the input value. This version is used for stored procedures. 
        /// </summary>
        /// <param name="field">Field to compare.</param>
        /// <param name="fieldValue">Value to compare.</param>
        /// <param name="allowEmpty">Allow empty string.</param>
        public void Add(string field, object fieldValue, bool allowEmpty)
        {
            if (fieldValue == null)
                return;
            if (allowEmpty == false && fieldValue is string &&
                ((string)fieldValue).Trim().Length == 0)
                return;

            if (fieldValue is Operator)
            {
                if (fieldValue.ToString() == "NULL")
                {
                    /***
                     * Implement the use of NULL in stored procedure parameters
                     ******/
                    _bindings.Add(new Clause(field, ""));
                }
                else
                {
                    Add(field, Logical.And, (Operator)fieldValue);
                }
            }
            else
            {
                if (fieldValue is DateTime)
                {
                    if ((DateTime)fieldValue == DateTime.MinValue)
                    {
                        _bindings.Add(new Clause(Logical.And, field, Operator.IsNull, fieldValue));
                    }
                    else
                    {
                        _bindings.Add(new Clause(field, fieldValue));
                    }
                }
                else
                {
                    _bindings.Add(new Clause(field, fieldValue));
                }
            }
        }// END OF Add

        /// <summary>
        /// Add a comparison to the list.
        /// </summary>
        /// <param name="field">Field to search.</param>
        /// <param name="fieldValue">Value to search.</param>
        /// <param name="logical">Boolean connector to other criteria.</param>
        public void Add(string field, object fieldValue, Logical logical)
        {
            if (fieldValue == null)
                return;
            if (fieldValue is string && ((string)fieldValue).Length == 0)
                return;
            if (fieldValue is DateTime)
            {
                if ((DateTime)fieldValue == DateTime.MinValue)
                {
                    _bindings.Add(new Clause(Logical.And, field, Operator.IsNull, fieldValue));
                }
                else
                {
                    _bindings.Add(new Clause(logical, field, Operator.Equal, fieldValue));
                }
            }
            else
            {
                _bindings.Add(new Clause(logical, field, Operator.Equal, fieldValue));
            }
        }// END OF Add


        ///<summary>
        ///</summary>
        ///<param name="field"></param>
        ///<param name="fieldValue"></param>
        ///<param name="searchOperator"></param>
        public void Add(Enum field, object fieldValue, Operator searchOperator)
        {
            Add(field.ToString(), fieldValue, searchOperator, Logical.And);
        }// END OF Add

        /// <summary>
        /// Add a comparison to the list.
        /// </summary>
        /// <param name="field">Field to search.</param>
        /// <param name="fieldValue">Value to search.</param>
        /// <param name="operator">Search operator.</param>
        public void Add(string field, object fieldValue, Operator @operator)
        {
            Add(field, fieldValue, @operator, Logical.And);
        }// END OF Add

        /// <summary>
        /// Add a comparison to the list.
        /// </summary>
        /// <param name="field">Field to search.</param>
        /// <param name="fieldValue">Value to search.</param>
        /// <param name="operator">Search operator.</param>
        /// <param name="logical">Boolean connector to other criteria.</param>
        public void Add(string field, object fieldValue,
                               Operator @operator, Logical logical)
        {
            if (fieldValue == null)
                return;
            if (fieldValue is string && ((string)fieldValue).Trim().Length == 0)
                return;
            if (fieldValue is DateTime)
            {
                if ((DateTime)fieldValue == DateTime.MinValue)
                {
                    _bindings.Add(new Clause(Logical.And, field, Operator.IsNull, fieldValue));
                }
                else
                {
                    _bindings.Add(new Clause(logical, field, @operator, fieldValue));
                }
            }
            else
            {
                _bindings.Add(new Clause(logical, field, @operator, fieldValue));
            }
        }// END OF Add


        ///<summary>
        ///</summary>
        ///<param name="field"></param>
        ///<param name="searchBoolean"></param>
        ///<param name="searchOperator"></param>
        public void Add(Enum field, Logical searchBoolean, Operator searchOperator)
        {
            Add(field.ToString(), searchBoolean, searchOperator);
        }

        /// <summary>
        /// Adds a comparison to the list. Used for "IS NULL" and "IS NOT NULL"
        /// </summary>
        /// <param name="field">Field to search.</param>
        /// <param name="logical">Boolean connector to other criteria.</param>
        /// <param name="operator">Search operator.</param>
        public void Add(string field, Logical logical, Operator @operator)
        {
            if (@operator == Operator.IsNull || @operator == Operator.IsNotNull)
                _bindings.Add(new Clause(logical, field, @operator, @operator.ToString()));
        }// END OF Add



        /// <summary>
        /// Add a sort order to the where clause.
        /// </summary>
        /// <param name="attribute">String with sort criteria.</param>
        public void AddSort(string attribute)
        {
            _sorts.Add(attribute);
        }// END OF addSort


        /// <summary>
        /// Add a group to the where clause.
        /// </summary>
        /// <param name="attribute">String with sort criteria.</param>
        public void AddGroup(string attribute)
        {
            _sorts.Add(attribute);
        }// END OF addSort


        /// <summary>
        /// Clear current sort criteria.
        /// </summary>
        public void ClearSort()
        {
            _sorts.Clear();
        }

        /// <summary>
        /// Indicates that the query will return distinct values.
        /// </summary>
        public bool Distinct
        {
            get { return _isDistinct; }
            set { _isDistinct = value; }
        }
        /// <summary>
        /// Indicates that only the Top first records will be returned.
        /// </summary>
        public int Top
        {
            get { return _top; }
            set { _top = value; }
        }

        /// <summary>
        /// Indicates the maximum number of rows to return.
        /// </summary>
        public long MaxRows
        {
            get { return _maxRows; }
            set { _maxRows = value; }
        }

        /// <summary>
        /// Add a sort order to the where clause.
        /// </summary>
        /// <param name="attributes">String array of field names.</param>
        public void AddSorts(string[] attributes)
        {
            for (int i = 0; i < attributes.Length; i++)
            {
                _sorts.Add(attributes[i]);
            }
        }// END OF addSorts




        // ----------------------------------------------------------- Properties    

        /// <summary>
        /// Get current bindings.
        /// </summary>
        /// <returns></returns>
        internal IEnumerator Bindings()
        {
            return _bindings.GetEnumerator();
        }

        /// <summary>
        /// Get current fields to retrieve.
        /// </summary>
        /// <returns>Fields to retrieve.</returns>
        internal List<string> Projections()
        {
            return _projections;
        }


        /// <summary>
        /// Get current sort fields.
        /// </summary>
        /// <returns>Sort fields.</returns>
        internal IEnumerator Sorts()
        {
            return _sorts.GetEnumerator();
        }
        /// <summary>
        /// Get current sort fields.
        /// </summary>
        /// <returns>Sort fields.</returns>
        public string Customs
        {
            get
            {
                return _custom;
            }
            set
            {
                _custom = value;
            }

        }
        /// <summary>
        /// Makes a deep copy of the current entity.
        /// </summary>
        /// <returns>A copy of the current entity.</returns>
        public Filter Copy()
        {
            Filter sc = new Filter();
            sc._custom = _custom;
            sc._isDistinct = _isDistinct;
            sc._maxRows = _maxRows;
            sc._pageNumber = _pageNumber;
            sc._rowsPerPage = RowsPerPage;
            sc._top = _top;
            foreach (Clause o in _bindings)
            {
                sc._bindings.Add(o);
            }
            foreach (string s in _projections)
            {
                sc._projections.Add(s);
            }
            foreach (string s in _sorts)
            {
                sc._sorts.Add(s);
            }
            return sc;
        }

        public object Clone()
        {
            return this;
        }
    }
}